/**

This file is part of MaCI/GIMnet.

MaCI/GIMnet is free software: you can redistribute it and/or modify it 
under the terms of the GNU Lesser General Public License as published 
by the Free Software Foundation, either version 3 of the License, or 
(at your option) any later version.

MaCI/GIMnet is distributed in the hope that it will be useful, but WITHOUT 
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public 
License for more details.

You should have received a copy of the GNU Lesser General Public 
License along with GIMnet. (See COPYING.LESSER) If not, see 
<http://www.gnu.org/licenses/>.

**/
#include "picking.hpp"
#include <limits.h>
#include <cstdio>
#include <cstring>
#include <GL/glu.h>

#include "glutils.hpp"

/********** CGLPickable **********/

CGLPickable::CGLPickable()
:callback(NULL),
userData(NULL),
enabled(true)
{
  
}

void CGLPickable::DrawPickable()
{
  //do picking mode drawing
}

void CGLPickable::SetCallback(CGLPickCallback *callback)
{
  this->callback=callback;
}

CGLPickCallback* CGLPickable::GetCallback()
{
  return callback;
}

void CGLPickable::SetUserData(void *data)
{
  userData=data;
}

void* CGLPickable::GetUserData()
{
  return userData;
}

void CGLPickable::EnablePicking(bool enable)
{
  enabled=enable;
}

bool CGLPickable::IsPickingEnabled()
{
  return enabled;
}


/*********** CGLPickingContext ***********/

CGLPickingContext::CGLPickingContext()
:counter(1),
pickActive(false),
focusActive(false),
focusEventsEnabled(true)
{
  
}

void CGLPickingContext::SetProjectionMatrix(double* projection)
{
  memcpy(projectionMatrix, projection, sizeof(double)*16);
}

const double* CGLPickingContext::GetProjectionMatrix()
{
  return projectionMatrix;
}

void CGLPickingContext::SetModelviewMatrix(double* modelview)
{
  memcpy(modelviewMatrix, modelview, sizeof(double)*16);
}

const double* CGLPickingContext::GetModelviewMatrix()
{
  return modelviewMatrix;
}

void CGLPickingContext::OnMouseDown(const int x, const int y, const int button)
{
  //take events only from one button at a time
  if (pickActive)
    return;
    
  float glx, gly, glz;
  CGLPickable *p=pick(x,y, glx,gly,glz);
  if (p)
  {
    TGLPickEvent e;
    e.Button=button;
    e.ScreenX=x;
    e.ScreenY=y;
    e.GlX=glx;
    e.GlY=gly;
    e.GlZ=glz;
    e.Pickable=p;
    lastEvent=e;
    pickActive=true;
    p->GetCallback()->OnPick(e);
  }
}

void CGLPickingContext::OnMouseUp(const int x,const int y)
{
  if (pickActive)
  {
    TGLPickEvent e;
    e=lastEvent;
    e.ScreenX=x;
    e.ScreenY=y;
    screenToGl(x, y, lastScreenZ, e.GlX, e.GlY, e.GlZ);
    if (lastEvent.Pickable)
      lastEvent.Pickable->GetCallback()->OnRelease(e);
    pickActive=false;
  }
}

void CGLPickingContext::OnMouseMove(const int x,const int y)
{
  if (pickActive)
  {
    TGLPickEvent e;
    e=lastEvent;
    e.ScreenX=x;
    e.ScreenY=y;
    screenToGl(x, y, lastScreenZ, e.GlX, e.GlY, e.GlZ);
    if (lastEvent.Pickable)
      lastEvent.Pickable->GetCallback()->OnDrag(e);
    
  } else if (focusEventsEnabled) {
    
    float glx, gly, glz;
    CGLPickable *p=pick(x,y, glx, gly, glz);
    if (p)
    {
      TGLPickEvent e;
      e.Button=0;
      e.ScreenX=x;
      e.ScreenY=y;
      e.GlX=glx;
      e.GlY=gly;
      e.GlZ=glz;
      e.Pickable=p;
      
      if (!focusActive) {
        //pickable was focused
        p->GetCallback()->OnFocus(e);
        focusActive=true;
        lastEvent=e;
      } else if (focusActive && p!=lastEvent.Pickable) {
        //focus was lost and set to other pickable
        p->GetCallback()->OnFocus(e);
        if (lastEvent.Pickable)
          lastEvent.Pickable->GetCallback()->OnFocusLost(e);
        lastEvent=e;
      }
       
    } else if (!p) {
      if (focusActive)
      {
        TGLPickEvent e;
        e.Button=0;
        e.ScreenX=x;
        e.ScreenY=y;
        e.GlX=glx;
        e.GlY=gly;
        e.GlZ=glz;
        e.Pickable=p;
        
        //focus was lost
        if (lastEvent.Pickable)
          lastEvent.Pickable->GetCallback()->OnFocusLost(e);
      }
      focusActive=false;
    }
    
    
  }
}

bool CGLPickingContext::AddPickable(CGLPickable *pickable)
{
  Lock();
  unsigned int ID=0;
  //generate unique id for every pickable
  if (counter<UINT_MAX)
  {
    //create id from static counter
    ID=counter;
    counter++;
  } else {
    //if counter has reached uint maximum value, find first free id from map
    unsigned int c=1;
    while (pickables.find(c)!=pickables.end() && c <=UINT_MAX)
    {
      c++;
    }
  }
  
  //finally add to map or complain
  bool ret;
  if (ID)
  {
    std::pair<int, CGLPickable*> p(ID, pickable);
    ret= pickables.insert(p).second;
  } else {
    ret= false;
  }
  Unlock();
  return ret;
}

void CGLPickingContext::RemovePickable(CGLPickable *pickable)
{
  Lock();
  std::map<GLuint,CGLPickable*>::iterator i;
  for (i=pickables.begin(); i!=pickables.end(); i++)
  {
    if ( i->second==pickable )
    {
      if (i->second==lastEvent.Pickable)
        lastEvent.Pickable=NULL;
      pickables.erase(i);
      break;
    }
  }
  Unlock();
}

void CGLPickingContext::EnableFocusEvents(bool enable)
{
  focusEventsEnabled=enable;
}
    
bool CGLPickingContext::FocusEventsEnabled()
{
  return focusEventsEnabled;
}

CGLPickable* CGLPickingContext::pick(const int x, const int y, float &glx, float &gly, float &glz)
{
  CGLPickable *p=NULL;
  GLint viewport[4];
  
  glMatrixMode(GL_PROJECTION);
  glPushMatrix();
  glLoadIdentity();
  
  glGetIntegerv(GL_VIEWPORT,viewport);
  
  gluPickMatrix(x,viewport[3]-y,
                1,1,viewport);
  
  //apply projection
  glMultMatrixd(projectionMatrix);
  
  glSelectBuffer(SELECT_BUFFER_SIZE, selectBuffer);
  
  glRenderMode(GL_SELECT);
  
  glInitNames();
  glPushName(0);
  
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();
  
  //apply modelview
  glMultMatrixd(modelviewMatrix);
  
  Lock();
  for (std::map<GLuint,CGLPickable*>::iterator i=pickables.begin(); i!=pickables.end(); i++)
  {
    glLoadName(i->first);
    if (i->second->IsPickingEnabled())
      i->second->DrawPickable();
  }
  glPopMatrix();
  
  glMatrixMode(GL_PROJECTION);
  glPopMatrix();
  //glMatrixMode(GL_MODELVIEW);
  glFlush();
  
  //calculate new z values only when picking, use same z on case of dragging and releasing
  //this causes dragging to happen on a plane parallel to screen
  glReadPixels( x, viewport[3]-y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &lastScreenZ );
  
  screenToGl(x,y,lastScreenZ,glx,gly,glz);
  
  GLint hits=glRenderMode(GL_RENDER);
  
  if (hits>0)
  {
    std::map<GLuint,CGLPickable*>::iterator i;
    //check the selection buffer contents and call callback for the topmost object
    i=pickables.find(getTopmostHit(hits));
    if (i!=pickables.end())
      p=i->second;
  }
  Unlock();
  return p;
}


GLuint CGLPickingContext::getTopmostHit(const GLint hits)
{
  int i;
  GLuint names, *ptr, minZ,*ptrNames, numberOfNames;

  ptr = (GLuint *) selectBuffer;
  ptrNames = ptr;
  minZ = 0xffffffff;
  for (i = 0; i < hits; i++) { 
    names = *ptr;
    ptr++;
    if (*ptr < minZ) {
      numberOfNames = names;
      minZ = *ptr;
      ptrNames = ptr+2;
    }
    
    ptr += names+2;
  }
  
  return ptrNames[0];

}

